
/*
 * jjyun_down.c
 * 下载文件功能
 * Created on: 2018年5月11日
 * Author: cfm
 */
#define RANGE_PREFIX_STR "bytes="
#define RANGE_PREFIX_LEN (int)(sizeof(RANGE_PREFIX_STR) - 1)

typedef struct jjyun_down_range_s
{
    int64_t start;
    int64_t end;
} jjyun_down_range_t;

typedef struct jjyun_down_response_s
{
    time_t last_modified; //last modified time of the file
    int range_len;
    int content_range_len;
    int64_t content_length;
    char *content_type;
    char *attachment_filename;
    char content_disposition[128];
    char range[64];
    char content_range[64];
    char last_modified_buff[32];
    bool header_outputed; //if header output
} jjyun_down_response_t;

typedef struct jjyun_fdfs_file_info_s
{
    int storage_port;
    FDFSStorePaths *pStorePaths;
    bool bSameGroup;
    int store_path_index;
    char *filename;
    int filename_len;
    char true_filename[128];
    FDFSFileInfo file_info;
    FDFSTrunkFullInfo trunkInfo;
    bool bFileExists;
    int64_t file_size;
    struct stat file_stat;
} jjyun_fdfs_file_info_t;

typedef struct jjyun_down_ctx_s
{
    ngx_http_request_t *r;
    bool header_only;
    bool if_range;
    jjyun_down_range_t range;
    char file_id[128];
    char if_modified_since[32];
} jjyun_down_ctx_t;

typedef struct jjyun_download_callback_args_s
{
    jjyun_down_ctx_t *pContext;
    jjyun_down_response_t *pResponse;
    int64_t sent_bytes; //sent bytes
} jjyun_download_callback_args_t;

static int fdfs_strtoll(const char *s, int64_t *value)
{
    char *end = NULL;
    *value = strtoll(s, &end, 10);
    if (end != NULL && *end != '\0')
    {
        return EINVAL;
    }

    return 0;
}

static int fdfs_format_http_datetime(time_t t, char *buff, const int buff_size)
{
    struct tm tm;
    struct tm *ptm;

    *buff = '\0';
    if ((ptm = gmtime_r(&t, &tm)) == NULL)
    {
        return errno != 0 ? errno : EFAULT;
    }

    strftime(buff, buff_size, "%a, %d %b %Y %H:%M:%S GMT", ptm);
    return 0;
}
/**
range format:
bytes=500-999
bytes=-500
bytes=9500-
*/
static int fdfs_parse_range(const char *value, jjyun_down_range_t *range)
{
    int len;
    int result;
    const char *p;
    const char *pEndPos;
    char buff[32];

    len = strlen(value);
    if (len <= RANGE_PREFIX_LEN + 1)
    {
        return EINVAL;
    }

    p = value + RANGE_PREFIX_LEN;
    if (*p == '-')
    {
        if ((result = fdfs_strtoll(p, &(range->start))) != 0)
        {
            return result;
        }
        range->end = 0;
        return 0;
    }

    pEndPos = strchr(p, '-');
    if (pEndPos == NULL)
    {
        return EINVAL;
    }

    len = pEndPos - p;
    if (len >= (int)sizeof(buff))
    {
        return EINVAL;
    }
    memcpy(buff, p, len);
    *(buff + len) = '\0';
    if ((result = fdfs_strtoll(buff, &(range->start))) != 0)
    {
        return result;
    }

    pEndPos++; //skip -
    if (*pEndPos == '\0')
    {
        range->end = 0;
    }
    else
    {
        if ((result = fdfs_strtoll(pEndPos, &(range->end))) != 0)
        {
            return result;
        }
    }

    return 0;
}

static ngx_int_t fdfs_parse_request_headers(jjyun_down_ctx_t *ctx)
{
    ngx_http_request_t *r = ctx->r;
    ngx_str_t t_fileid;

    if (ngx_http_arg(r, (u_char *)"fileid", ngx_strlen("fileid"), &t_fileid) != NGX_OK)
    {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    ngx_snprintf((u_char *)ctx->file_id, sizeof(ctx->file_id), "%V%Z", &t_fileid);

    if (r->headers_in.if_modified_since != NULL)
    {
        if (r->headers_in.if_modified_since->value.len < sizeof(ctx->if_modified_since))
        {
            memcpy(ctx->if_modified_since, r->headers_in.if_modified_since->value.data,
                   r->headers_in.if_modified_since->value.len);
        }
    }

    if (r->headers_in.range != NULL)
    {
        char buff[64];
        if (r->headers_in.range->value.len >= sizeof(buff))
        {
            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                          "bad request, range length: %d exceeds buff "
                          "size: %d, range: %*s",
                          r->headers_in.range->value.len,
                          (int)sizeof(buff),
                          r->headers_in.range->value.len,
                          r->headers_in.range->value.data);
            return NGX_HTTP_BAD_REQUEST;
        }

        memcpy(buff, r->headers_in.range->value.data, r->headers_in.range->value.len);
        *(buff + r->headers_in.range->value.len) = '\0';
        if (fdfs_parse_range(buff, &(ctx->range)) != 0)
        {
            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "bad request, invalid range: %s", buff);
            return NGX_HTTP_BAD_REQUEST;
        }
        ctx->if_range = true;
    }
    return NGX_OK;
}

static ngx_int_t fdfs_set_content_disposition(ngx_http_request_t *r, jjyun_down_response_t *pResponse)
{
    int value_len;
    value_len = snprintf(pResponse->content_disposition,
                         sizeof(pResponse->content_disposition),
                         "attachment; filename=\"%s\"", pResponse->attachment_filename);
    return jjyun_set_header(r, "Content-Disposition", "content-disposition",
                            sizeof("Content-Disposition") - 1,
                            pResponse->content_disposition, value_len);
}

static ngx_int_t fdfs_set_content_range(ngx_http_request_t *r, jjyun_down_response_t *pResponse)
{
    return jjyun_set_header(r, "Content-Range", "content-range",
                            sizeof("Content-Range") - 1, pResponse->content_range,
                            pResponse->content_range_len);
}

static ngx_int_t fdfs_set_accept_ranges(ngx_http_request_t *r)
{
    return jjyun_set_header(r, "Accept-Ranges", "accept-ranges",
                            sizeof("Accept-Ranges") - 1, "bytes", sizeof("bytes") - 1);
}

static void fdfs_output_headers(ngx_http_request_t *r, jjyun_down_response_t *pResponse, int status)
{
    ngx_int_t rc;

    if (pResponse->header_outputed)
    {
        return;
    }

    if (status != NGX_HTTP_OK && status != NGX_HTTP_PARTIAL_CONTENT)
    {
        return;
    }
    else
    {
        if (pResponse->content_type != NULL)
        {
            r->headers_out.content_type.len = strlen(pResponse->content_type);
            r->headers_out.content_type.data = (u_char *)pResponse->content_type;
        }

        r->headers_out.content_length_n = pResponse->content_length;
        if (pResponse->attachment_filename != NULL)
        {
            fdfs_set_content_disposition(r, pResponse);
        }

        r->headers_out.last_modified_time = pResponse->last_modified;
        fdfs_set_accept_ranges(r);
        if (pResponse->content_range_len > 0)
        {
            fdfs_set_content_range(r, pResponse);
        }
    }

    ngx_http_set_content_type(r);

    r->headers_out.status = status;
    pResponse->header_outputed = true;
    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK)
    {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "ngx_http_send_header fail, return code=%d", rc);
        return;
    }
}

static int fdfs_download_callback(void *arg, const int64_t file_size, const char *data, const int current_size)
{
    jjyun_download_callback_args_t *pCallbackArgs;

    pCallbackArgs = (jjyun_download_callback_args_t *)arg;

    if (!pCallbackArgs->pResponse->header_outputed)
    {
        pCallbackArgs->pResponse->content_length = file_size;
        fdfs_output_headers(pCallbackArgs->pContext->r,
                            pCallbackArgs->pResponse, NGX_HTTP_OK);
    }

    pCallbackArgs->sent_bytes += current_size;
    return jjyun_send_reply_chunk(
        pCallbackArgs->pContext->r,
        (pCallbackArgs->sent_bytes == file_size) ? 1 : 0,
        data, current_size);
}

static void fdfs_format_content_range(const jjyun_down_range_t *range, const int64_t file_size, jjyun_down_response_t *pResponse)
{
    pResponse->content_range_len = sprintf(pResponse->content_range,
                                           "bytes " INT64_PRINTF_FORMAT "-" INT64_PRINTF_FORMAT "/" INT64_PRINTF_FORMAT,
                                           range->start, range->end, file_size);
}

static int fdfs_check_and_format_range(jjyun_down_range_t *range, const int64_t file_size)
{
    if (range->start < 0)
    {
        int64_t start;
        start = range->start + file_size;
        if (start < 0)
        {
            logError("file: "__FILE__
                     ", line: %d, "
                     "invalid range value: " INT64_PRINTF_FORMAT,
                     __LINE__, range->start);
            return EINVAL;
        }
        range->start = start;
    }
    else if (range->start >= file_size)
    {
        logError("file: "__FILE__
                 ", line: %d, "
                 "invalid range start value: " INT64_PRINTF_FORMAT
                 ", exceeds file size: " INT64_PRINTF_FORMAT,
                 __LINE__, range->start, file_size);
        return EINVAL;
    }

    if (range->end == 0)
    {
        range->end = file_size - 1;
    }
    else if (range->end >= file_size)
    {
        logError("file: "__FILE__
                 ", line: %d, "
                 "invalid range end value: " INT64_PRINTF_FORMAT
                 ", exceeds file size: " INT64_PRINTF_FORMAT,
                 __LINE__, range->end, file_size);
        return EINVAL;
    }

    if (range->start > range->end)
    {
        logError("file: "__FILE__
                 ", line: %d, "
                 "invalid range value, start: " INT64_PRINTF_FORMAT
                 ", exceeds end: " INT64_PRINTF_FORMAT,
                 __LINE__, range->start, range->end);
        return EINVAL;
    }

    return 0;
}

#define FDFS_SET_LAST_MODIFIED(pResponse, pContext, mtime)                          \
    do                                                                              \
    {                                                                               \
        pResponse->last_modified = mtime;                                           \
        fdfs_format_http_datetime(pResponse->last_modified,                         \
                                  pResponse->last_modified_buff,                    \
                                  sizeof(pResponse->last_modified_buff));           \
        if (*pContext->if_modified_since != '\0')                                   \
        {                                                                           \
            if (strcmp(pResponse->last_modified_buff,                               \
                       pContext->if_modified_since) == 0)                           \
            {                                                                       \
                fdfs_output_headers(pContext->r, pResponse, NGX_HTTP_NOT_MODIFIED); \
                return NGX_HTTP_NOT_MODIFIED;                                       \
            }                                                                       \
        }                                                                           \
    } while (0)

static ngx_int_t fdfs_get_fileinfo(char *file_id, jjyun_fdfs_file_info_t *pfile, jjyun_down_ctx_t *ctx, jjyun_down_response_t *pResponse)
{
    char *file_id_without_group;
    int group_name_len;
    int fd;
    int result;

    pfile->storage_port = storage_server_port;
    pfile->pStorePaths = &g_fdfs_store_paths;

    file_id_without_group = strchr(file_id, '/');
    if (file_id_without_group == NULL)
    {
        return NGX_HTTP_BAD_REQUEST;
    }

    group_name_len = file_id_without_group - file_id;
    if (group_count == 0)
    {
        pfile->bSameGroup = (group_name_len == my_group_name_len) && (memcmp(file_id, my_group_name, group_name_len) == 0);
    }
    else
    {
        int i;

        pfile->bSameGroup = false;
        for (i = 0; i < group_count; i++)
        {
            if (group_store_paths[i].group_name_len == group_name_len &&
                memcmp(file_id, group_store_paths[i].group_name, group_name_len) == 0)
            {
                pfile->bSameGroup = true;
                pfile->storage_port = group_store_paths[i].storage_server_port;
                pfile->pStorePaths = &group_store_paths[i].store_paths;
                break;
            }
        }
    }

    file_id_without_group++; //skip /

    if (strlen(file_id_without_group) < 22)
    {
        return NGX_HTTP_BAD_REQUEST;
    }

    pfile->filename = file_id_without_group;
    pfile->filename_len = strlen(pfile->filename);

    if (storage_split_filename_no_check(pfile->filename, &pfile->filename_len, pfile->true_filename, &pfile->store_path_index) != 0)
    {
        return NGX_HTTP_BAD_REQUEST;
    }
    if (pfile->bSameGroup)
    {
        if (pfile->store_path_index < 0 || pfile->store_path_index >= pfile->pStorePaths->count)
        {
            return NGX_HTTP_BAD_REQUEST;
        }
    }

    if (fdfs_check_data_filename(pfile->true_filename, pfile->filename_len) != 0)
    {
        return NGX_HTTP_BAD_REQUEST;
    }

    if (fdfs_get_file_info_ex1(file_id, false, &pfile->file_info) != 0)
    {
        return NGX_HTTP_BAD_REQUEST;
    }

    if (pfile->file_info.file_size >= 0) //mormal file
    {
        FDFS_SET_LAST_MODIFIED(pResponse, ctx, pfile->file_info.create_timestamp);
    }

    fd = -1;
    memset(&pfile->file_stat, 0, sizeof(pfile->file_stat));
    if (pfile->bSameGroup)
    {
        FDFSTrunkHeader trunkHeader;
        if (trunk_file_stat_ex1(pfile->pStorePaths, pfile->store_path_index,
                                pfile->true_filename, pfile->filename_len, &pfile->file_stat,
                                &pfile->trunkInfo, &trunkHeader, &fd) != 0)
        {
            pfile->bFileExists = false;
        }
        else
        {
            pfile->bFileExists = true;
        }
    }
    else
    {
        pfile->bFileExists = false;
        memset(&pfile->trunkInfo, 0, sizeof(pfile->trunkInfo));
    }
    if (fd >= 0)
    {
        close(fd);
    }

    if (pfile->bFileExists)
    {
        if (pfile->file_size < 0) //slave or appender file
        {
            FDFS_SET_LAST_MODIFIED(pResponse, ctx, pfile->file_stat.st_mtime);
        }
    }
    else
    {
        if (pfile->bSameGroup && (is_local_host_ip(pfile->file_info.source_ip_addr) ||
                                  (pfile->file_info.create_timestamp > 0 && (time(NULL) - pfile->file_info.create_timestamp > storage_sync_file_max_delay))))
        {
            return NGX_HTTP_NOT_FOUND;
        }
    }

    if (pfile->bFileExists)
    {
        pfile->file_size = pfile->file_stat.st_size;
    }
    else
    {
        bool if_get_file_info;
        if_get_file_info = ctx->header_only || (ctx->if_range && pfile->file_info.file_size < 0);
        if (if_get_file_info)
        {
            if ((result = fdfs_get_file_info_ex1(file_id, true, &pfile->file_info)) != 0)
            {
                if (result == ENOENT)
                {
                    return NGX_HTTP_NOT_FOUND;
                }
                else
                {
                    return NGX_HTTP_INTERNAL_SERVER_ERROR;
                }
            }
        }

        pfile->file_size = pfile->file_info.file_size;
    }

    return NGX_OK;
}

static ngx_int_t jjyun_down_file(jjyun_down_ctx_t *ctx)
{
    ngx_int_t rc;
    char *file_id = ctx->file_id;
    jjyun_fdfs_file_info_t file;
    jjyun_down_response_t response;
    int64_t file_offset;
    int64_t download_bytes;
    int result;
    bool bTrunkFile;
    char full_filename[MAX_PATH_SIZE + 64];
    int full_filename_len;

    memset(&response, 0, sizeof(response));
    response.attachment_filename = strrchr(file_id, '/') + 1;

    rc = fdfs_get_fileinfo(file_id, &file, ctx, &response);
    if (rc == NGX_HTTP_NOT_MODIFIED)
    {
        return rc;
    }
    if (rc != NGX_OK)
    {
        fdfs_output_headers(ctx->r, &response, rc);
        return rc;
    }

    if (ctx->if_range)
    {
        if (fdfs_check_and_format_range(&(ctx->range), file.file_size) != 0)
        {
            return NGX_HTTP_BAD_REQUEST;
        }

        download_bytes = (ctx->range.end - ctx->range.start) + 1;
        fdfs_format_content_range(&(ctx->range), file.file_size, &response);
    }
    else
    {
        download_bytes = file.file_size > 0 ? file.file_size : 0;
    }

    if (ctx->header_only)
    {
        response.content_length = download_bytes;
        fdfs_output_headers(ctx->r, (&response), ctx->if_range ? NGX_HTTP_PARTIAL_CONTENT : NGX_HTTP_OK);
        return NGX_HTTP_OK;
    }

    if (!file.bFileExists)
    {
        ConnectionInfo storage_server;
        jjyun_download_callback_args_t callback_args;
        int64_t file_size;

        strcpy(storage_server.ip_addr, file.file_info.source_ip_addr);
        storage_server.port = file.storage_port;
        storage_server.sock = -1;

        callback_args.pContext = ctx;
        callback_args.pResponse = &response;
        callback_args.sent_bytes = 0;

        result = storage_download_file_ex1(NULL,
                                           &storage_server, file_id,
                                           ctx->range.start, download_bytes,
                                           fdfs_download_callback, &callback_args, &file_size);

        logDebug("file: "__FILE__
                 ", line: %d, "
                 "storage_download_file_ex1 return code: %d, "
                 "file id: %s",
                 __LINE__, result, file_id);

        if (result == 0)
        {
            fdfs_output_headers(ctx->r, (&response), NGX_HTTP_OK);
            return NGX_OK;
        }
        if (result == ENOENT)
        {
            return NGX_HTTP_NOT_FOUND;
        }
        else
        {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }

    bTrunkFile = IS_TRUNK_FILE_BY_ID(file.trunkInfo);
    if (bTrunkFile)
    {
        trunk_get_full_filename_ex(file.pStorePaths, &file.trunkInfo,
                                   full_filename, sizeof(full_filename));
        full_filename_len = strlen(full_filename);
        file_offset = TRUNK_FILE_START_OFFSET(file.trunkInfo) + ctx->range.start;
    }
    else
    {
        full_filename_len = snprintf(full_filename,
                                     sizeof(full_filename), "%s/data/%s",
                                     file.pStorePaths->paths[file.store_path_index],
                                     file.true_filename);
        file_offset = ctx->range.start;
    }

    response.content_length = download_bytes;
    fdfs_output_headers(ctx->r, (&response), ctx->if_range ? NGX_HTTP_PARTIAL_CONTENT : NGX_HTTP_OK);
    return jjyun_send_file(ctx->r, full_filename, full_filename_len, file_offset, download_bytes);
}

static ngx_int_t jjyun_down_handler(ngx_http_jjyun_ctx_t *jjctx)
{
    ngx_int_t rc;
    ngx_http_request_t *r = jjctx->r;
    if (NULL == jjctx->type_ctx)
    {
        jjctx->type_ctx = ngx_pcalloc(r->pool, sizeof(jjyun_down_ctx_t));
        if (jjctx->type_ctx == NULL)
        {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }
    jjyun_down_ctx_t *u = (jjyun_down_ctx_t *)jjctx->type_ctx;
    u->r = jjctx->r;
    u->header_only = r->header_only || r->method == NGX_HTTP_HEAD;

    if ((rc = fdfs_parse_request_headers(u)) != NGX_OK)
        return rc;

    if ((rc = jjyun_down_file(u)) != NGX_OK)
        return rc;

    return rc;
}